﻿using System;
using System.Collections.Generic;
using System.Threading;
using Pfz.Caching;
using Pfz.Threading;

namespace Pfz
{
	/// <summary>
	/// Class responsible for calling dispose of its value, even if this class itself is not properly disposed.
	/// </summary>
	public class DisposeAssurerBase:
		IAdvancedDisposable
	{
		internal static readonly object _lock = new object();
		internal static Dictionary<IDisposable, int> _dictionary = new Dictionary<IDisposable, int>(ReferenceComparer.Instance);
		private static List<IDisposable> _toDispose = new List<IDisposable>();
		private static ManagedAutoResetEvent _autoResetEvent = new ManagedAutoResetEvent();
		static DisposeAssurerBase()
		{
			GCUtils.Collected += _Collected;
			var domain = AppDomain.CurrentDomain;
			domain.DomainUnload += _DisposeAll;
			domain.ProcessExit += _DisposeAll;

			Thread thread = new Thread(_Run);
			thread.Name = "DisposeCaller";
			thread.Start();
		}

		private static void _DisposeAll(object sender, EventArgs e)
		{
			GCUtils.Collected -= _Collected;

			Dictionary<IDisposable, int> dictionary;
			List<IDisposable> toDispose;
			lock(_lock)
			{
				dictionary = _dictionary;
				toDispose = _toDispose;

				_dictionary = null;
				_toDispose = null;
			}

			_autoResetEvent.Set();

			if (toDispose != null)
				foreach(var disposable in toDispose)
					disposable.Dispose();

			if (dictionary != null)
				foreach(var disposable in dictionary.Keys)
					disposable.Dispose();
		}

		private static void _Collected()
		{
			lock(_lock)
			{
				var hashset = _dictionary;
				if (hashset == null)
				{
					GCUtils.Collected -= _Collected;
					return;
				}

				_dictionary = new Dictionary<IDisposable,int>(hashset);
			}
		}

		private static void _Run()
		{
			var thread = Thread.CurrentThread;
			while(true)
			{
				thread.IsBackground = true;
				_autoResetEvent.WaitOne();
				thread.IsBackground = false;

				while(true)
				{
					List<IDisposable> toDispose;
					lock(_lock)
					{
						toDispose = _toDispose;
						if (toDispose == null)
							return;

						int count = toDispose.Count;
						if (count == 0)
							break;

						_toDispose = new List<IDisposable>();
					}

					foreach(var disposable in toDispose)
						disposable.Dispose();
				}
			}
		}

		internal DisposeAssurerBase()
		{
		}

		/// <summary>
		/// When inheriting this class, you should provide the value.
		/// If the value can't be guaranteed to be disposed later (system is out of memory now)
		/// it is disposed immediatelly and an exception is thrown.
		/// </summary>
		protected DisposeAssurerBase(IDisposable value)
		{
			if (value == null)
				throw new ArgumentNullException("value");

			_value = value;
			try
			{
				lock(_lock)
				{
					int referenceCount;
					_dictionary.TryGetValue(value, out referenceCount);
					referenceCount++;
					_dictionary[value] = referenceCount;
				}
			}
			catch
			{
				GC.SuppressFinalize(this);
				value.Dispose();
				_value = null;
				throw;
			}
		}

		/// <summary>
		/// Immediatelly releases all resources used by this object.
		/// </summary>
		public void Dispose()
		{
			GC.SuppressFinalize(this);

			IDisposable value;
			lock(_lock)
			{
				var hashset = _dictionary;
				if (hashset == null)
					return;

				value = _value;
				if (value == null)
					return;

				_value = null;
				if (!hashset.Remove(value))
					return;
			}

			value.Dispose();
		}

		/// <summary>
		/// Will try to dispose the referenced object.
		/// </summary>
		~DisposeAssurerBase()
		{
			// As threads can be stopped, we can't wait on lock. If we can't lock now, we need to retry later.
			if (!Monitor.TryEnter(_lock))
			{
				GC.ReRegisterForFinalize(this);
				return;
			}

			try
			{
				var value = _value;
				var dictionary = _dictionary;
				if (dictionary == null)
					return;

				int referenceCount;
				if (!dictionary.TryGetValue(value, out referenceCount))
					return;

				if (referenceCount > 1)
				{
					dictionary[value] = referenceCount-1;
					return;
				}

				dictionary.Remove(value);

				try
				{
					_toDispose.Add(value);
				}
				catch
				{
					GC.ReRegisterForFinalize(this);
					return;
				}

				if (_toDispose.Count == 1)
					_autoResetEvent.Set();
			}
			finally
			{
				Monitor.Exit(_lock);
			}
		}

		internal IDisposable _value;
		/// <summary>
		/// Gets the value of this DisposeCaller.
		/// </summary>
		protected IDisposable Value
		{
			get
			{
				return _value;
			}
		}

		/// <summary>
		/// Gets a value indicating if this object was already disposed.
		/// </summary>
		public bool WasDisposed
		{
			get
			{
				return _value == null;
			}
		}

		/// <summary>
		/// Gets the hashcode of the value;
		/// </summary>
		public override int GetHashCode()
		{
			if (_value == null)
				return 0;

			return _value.GetHashCode();
		}

		/// <summary>
		/// Verifies if two DisposeAssures point to the same value.
		/// </summary>
		public override bool Equals(object obj)
		{
			DisposeAssurerBase other = obj as DisposeAssurerBase;
			if (other == null)
				return false;

			return _value == other._value;
		}
	}

	/// <summary>
	/// Class responsible for calling dispose of its value, even if this class itself is not properly disposed.
	/// </summary>
	public class DisposeAssurerBase<T>:
		DisposeAssurerBase
	where
		T: class, IDisposable
	{
		internal DisposeAssurerBase()
		{
		}

		/// <summary>
		/// When inheriting this class, you should provide the value.
		/// If the value can't be guaranteed to be disposed later (system is out of memory now)
		/// it is disposed immediatelly and an exception is thrown.
		/// </summary>
		protected DisposeAssurerBase(T value):
			base(value)
		{
		}

		/// <summary>
		/// Gets the value of this DisposeCaller.
		/// </summary>
		protected new T Value
		{
			get
			{
				return (T)base.Value;
			}
		}
	}
}
